#include <graphics/GLSLProgram.h>
#include <graphics/GLSLFileFormat.h>
#include <graphics/RenderSystemDefine.h>
#include <system/Log.h>

namespace ne
{
    GLSLProgram::GLSLProgram(ResourceManager *pManager,
                             const uint64_t id,
                             const std::string &name,
                             const std::string &fileName,
                             const bool isManual,
                             ManualResourceLoader *pLoader)
        : Resource(pManager, id, name, fileName, isManual, pLoader),
          GLObject()
    {
        mResourceType = "GLSLShader";
        mSource = glCreateProgram();
    }

    GLSLShaderPtr GLSLProgram::createShaderFromFile(const std::string &name,
                                                    const std::string &fileName,
                                                    const ShaderType type)
    {
        GLSLShaderPtr pShader(create(name, fileName));
        if (pShader.notNull())
        {
            pShader->createInternalShader(type);
            pShader->load();
        }
        return pShader;
    }

    bool GLSLProgram::attachShader(const GLSLShaderPtr &pShader)
    {
        if (pShader.notNull() && pShader->isValidated())
        {
            AttachedShadersConstIterator it = mAttachedShaders.find(pShader->getSource());
            if (it == mAttachedShaders.end())
            {
                glAttachShader(mSource, pShader->getSource());
                mAttachedShaders[pShader->getSource()] = pShader.ptr();
                return true;
            }
        }
        return false;
    }

    bool GLSLProgram::detachShader(const GLSLShaderPtr &pShader)
    {
        if (pShader.notNull())
        {
            AttachedShadersIterator it = mAttachedShaders.find(pShader->getSource());
            if (it != mAttachedShaders.end())
            {
                glDetachShader(mSource, pShader->getSource());
                mAttachedShaders.erase(it);
                return true;
            }
        }
        return false;
    }

    void GLSLProgram::detachAll()
    {
        while (!mAttachedShaders.empty())
        {
            AttachedShadersConstIterator it = mAttachedShaders.begin();
            detachShader(GLSLShaderPtr(it->second));
        }
    }

    bool GLSLProgram::useProgram(const bool use)
    {
        if (use)
        {
            glUseProgram(mSource);
            return true;
        }

        glUseProgram(0);
        return false;
    }

    bool GLSLProgram::link()
    {
        glLinkProgram(mSource);
        if (!isLinked())
        {
            Log::Out(LL_WARNING,
                "GLSLProgram (%s) Link failed : %s", getName().c_str(), getInfoLog().c_str());
            return false;
        }

        return true;
    }

    bool GLSLProgram::validate()
    {
        glValidateProgram(mSource);
        if (!isValidated())
        {
            Log::Out(LL_WARNING,
                "GLSLProgram (%s) Validate failed : %s", getName().c_str(), getInfoLog().c_str());
            return false;
        }

        return true;
    }

    int GLSLProgram::getAttribLocation(const std::string &name) const
    {
        return glGetAttribLocation(mSource, name.c_str());
    }

    bool GLSLProgram::setAttribLocation(const uint32_t location, const std::string &name)
    {
        if (!isLinked())
        {
            glBindAttribLocation(mSource, location, name.c_str());
            return true;
        }
        return false;
    }

    int GLSLProgram::getUniformLocation(const std::string &name) const
    {
        return glGetUniformLocation(mSource, name.c_str());
    }

    bool GLSLProgram::setUniform(const int location, const int value)
    {
        glUniform1i(location, value);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const real value)
    {
        glUniform1f(location, value);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Vector2 &value)
    {
        glUniform2f(location, value.x, value.y);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Vector3 &value)
    {
        glUniform3f(location, value.x, value.y, value.z);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Vector4 &value)
    {
        glUniform4f(location, value.x, value.y, value.z, value.w);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Color &value)
    {
        glUniform4f(location, value.r, value.g, value.b, value.a);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Quaternion &value)
    {
        glUniform4f(location, value.x, value.y, value.z, value.w);
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Matrix3 &value, const bool transpose)
    {
        glUniformMatrix3fv(location, 1, transpose, value.ptr());
        return true;
    }

    bool GLSLProgram::setUniform(const int location, const Matrix4 &value, const bool transpose)
    {
        glUniformMatrix4fv(location, 1, transpose, value.ptr());
        return true;
    }

    bool GLSLProgram::setUniform(const std::string &name, const int value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform1i(location, value);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const real value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform1f(location, value);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Vector2 &value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform2f(location, value.x, value.y);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Vector3 &value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform3f(location, value.x, value.y, value.z);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Vector4 &value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform4f(location, value.x, value.y, value.z, value.w);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Color &value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform4f(location, value.r, value.g, value.b, value.a);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Quaternion &value)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniform4f(location, value.x, value.y, value.z, value.w);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Matrix3 &value, const bool transpose)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniformMatrix3fv(location, 1, transpose, value.ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniform(const std::string &name, const Matrix4 &value, const bool transpose)
    {
        int location = getUniformLocation(name);
        if (location >= 0)
        {
            glUniformMatrix4fv(location, 1, transpose, value.ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const int *pValue)
    {
        if (pValue)
        {
            glUniform1iv(location, count, pValue);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const real *pValue)
    {
        if (pValue)
        {
            glUniform1fv(location, count, pValue);
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Vector2 *pValue)
    {
        if (pValue)
        {
            glUniform2fv(location, count, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Vector3 *pValue)
    {
        if (pValue)
        {
            glUniform3fv(location, count, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Vector4 *pValue)
    {
        if (pValue)
        {
            glUniform4fv(location, count, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Color *pValue)
    {
        if (pValue)
        {
            glUniform4fv(location, count, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Quaternion *pValue)
    {
        if (pValue)
        {
            glUniform4fv(location, count, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Matrix3 *pValue, const bool transpose)
    {
        if (pValue)
        {
            glUniformMatrix3fv(location, count, transpose, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const int location, const uint32_t count, const Matrix4 *pValue, const bool transpose)
    {
        if (pValue)
        {
            glUniformMatrix4fv(location, count, transpose, pValue[0].ptr());
            return true;
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const int *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform1iv(location, count, pValue);
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const real *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform1fv(location, count, pValue);
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Vector2 *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform2fv(location, count, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Vector3 *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform3fv(location, count, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Vector4 *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform4fv(location, count, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Color *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform4fv(location, count, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Quaternion *pValue)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniform4fv(location, count, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Matrix3 *pValue, const bool transpose)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniformMatrix3fv(location, count, transpose, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::setUniformArray(const std::string &name, const uint32_t count, const Matrix4 *pValue, const bool transpose)
    {
        if (pValue)
        {
            int location = getUniformLocation(name);
            if (location >= 0)
            {
                glUniformMatrix4fv(location, count, transpose, pValue[0].ptr());
            }
        }
        return false;
    }

    bool GLSLProgram::isProgram() const
    {
        GLboolean res = glIsProgram(mSource);
        if (res)
        {
            return true;
        }
        return false;
    }

    bool GLSLProgram::isLinked() const
    {
        // Check link error
        int success = 0;
        glGetProgramiv(mSource, GL_LINK_STATUS, &success);
        return (success != 0);
    }

    bool GLSLProgram::isValidated() const
    {
        // Check Validate
        int success = 0;
        glGetProgramiv(mSource, GL_VALIDATE_STATUS, &success);
        return (success != 0);
    }

    bool GLSLProgram::isShaderAttached(const GLSLShaderPtr &pShader) const
    {
        if (pShader.notNull())
        {
            AttachedShadersConstIterator it = mAttachedShaders.find(pShader->getSource());
            if (it != mAttachedShaders.end())
            {
                return true;
            }
        }
        return false;
    }

    std::string GLSLProgram::getInfoLog() const
    {
        if (isProgram())
        {
            int infoLength = 0;
            glGetProgramiv(mSource, GL_INFO_LOG_LENGTH, &infoLength);
            char *info = new char[infoLength + 1];
            glGetProgramInfoLog(mSource, infoLength, 0, info);
            std::string infoLog = info;
            delete[] info;
            return infoLog;
        }
        return "";
    }

    ResourcePtr GLSLProgram::prepare(const std::string &name,
                                     const std::string &fileName,
                                     const bool isManual,
                                     ManualResourceLoader *pLoader,
                                     const bool backgroundThread)
    {
        return ResourceManager::prepare(name, fileName, isManual, pLoader, backgroundThread);
    }

    ResourcePtr GLSLProgram::load(const std::string &name,
                                  const std::string &fileName,
                                  const bool isManual,
                                  ManualResourceLoader *pLoader,
                                  const bool backgroundThread)
    {
        return ResourceManager::load(name, fileName, isManual, pLoader, backgroundThread);
    }

    bool GLSLProgram::prepare(const bool backgroundThread)
    {
        return Resource::prepare(backgroundThread);
    }
            
    bool GLSLProgram::load(const bool backgroundThread)
    {
        return Resource::load(backgroundThread);
    }

    GLSLProgram::~GLSLProgram()
    {
        unload();
        if (isProgram())
        {
            glDeleteProgram(mSource);
        }
    }

    bool GLSLProgram::loadImplement()
    {
        FILE *file = openFile("rb");
        if (!file)
        {
            Log::Out(LL_WARNING,
                "Open GLSLProgram (%s) file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }

        GLSLProgramHeader programHeader;
        if (!fread(&programHeader, sizeof(GLSLProgramHeader), 1, file))
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load GLSLProgram (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }

        if (programHeader.mMagicNum != GLSL_FILE_MAGIC_NUMBER ||
            programHeader.mVersion != GLSL_FILE_VERSION)
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load GLSLProgram (%s) from file (%s) failed with wrong Magic or Version", getName().c_str(), getFileName().c_str());
            return false;
        }

        // Seek to ShaderHeaders
        fseek(file, programHeader.mShaderOffset, SEEK_SET);

        // Create Shader
        GLSLShaderHeader shaderHeaders[programHeader.mNumShaders];
        if (!fread(shaderHeaders, sizeof(GLSLShaderHeader), programHeader.mNumShaders, file))
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load GLSLProgram (%s) from file (%s) failed with wrong Shader headers", getName().c_str(), getFileName().c_str());
            return false;
        }
        closeFile(file);

        for (uint32_t i=0; i<programHeader.mNumShaders; ++i)
        {
            std::string shaderName = shaderHeaders[i].mName;
            GLSLShaderPtr pShader(createShaderFromFile(shaderName, shaderName, ShaderType(shaderHeaders[i].mType)));
            attachShader(pShader);
        }

        return true;
    }

    bool GLSLProgram::unloadImplement()
    {
        detachAll();
        unloadAll(false);
        return true;
    }

    bool GLSLProgram::downloadImplement()
    {
        FILE *file = openFile("wb");
        if (!file)
        {
            Log::Out(LL_WARNING,
                "Create GLSLProgram (%s) file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }

        GLSLProgramHeader programHeader;
        memset(&programHeader, 0, sizeof(GLSLProgramHeader));

        programHeader.mMagicNum = GLSL_FILE_MAGIC_NUMBER;
        programHeader.mVersion = GLSL_FILE_VERSION;
        strncpy(programHeader.mName, mName.c_str(),
                MAX_STRING_LENGTH_IN_GLSL_FILE);
        programHeader.mNumShaders = getResourceCount();
        fwrite(&programHeader, sizeof(GLSLProgramHeader), 1, file);
        programHeader.mShaderOffset = ftell(file);
        fseek(file, 0, SEEK_SET);
        fwrite(&programHeader, sizeof(GLSLProgramHeader), 1, file);

        GLSLShaderHeader shaderHeaders[programHeader.mNumShaders];
        memset(shaderHeaders, 0, sizeof(GLSLShaderHeader) * programHeader.mNumShaders);

        ResourceMapConstIterator it = mResourceMap.begin();
        for (uint32_t i=0; i<programHeader.mNumShaders; ++i)
        {
            GLSLShaderPtr pShader(it->second);
            strncpy(shaderHeaders[i].mName, pShader->getFileName().c_str(),
                    MAX_STRING_LENGTH_IN_GLSL_FILE);
            shaderHeaders[i].mType = (int)pShader->getType();
            pShader->download();
        }

        fseek(file, programHeader.mShaderOffset, SEEK_SET);
        fwrite(shaderHeaders, sizeof(GLSLShaderHeader), programHeader.mNumShaders, file);
        closeFile(file);

        return true;
    }

    size_t GLSLProgram::_calculateSize() const
    {
        size_t size = 0;
        for (ResourceMapConstIterator it=mResourceMap.begin(); it!=mResourceMap.end(); ++it)
        {
            size += it->second->getSize();
        }
        return size;
    }

    Resource* GLSLProgram::createImplement(const uint64_t id,
                                           const std::string &name,
                                           const std::string &fileName,
                                           const bool isManual,
                                           ManualResourceLoader *pLoader)
    {
        return new GLSLShader(this, id, name, fileName, isManual, pLoader);
    }
}
